Skip to content

feat: extend deeplinks + add Raycast extension#1690

Open
macakii327-prog wants to merge 2 commits intoCapSoftware:mainfrom
macakii327-prog:feat/deeplinks-raycast-extension
Open

feat: extend deeplinks + add Raycast extension#1690
macakii327-prog wants to merge 2 commits intoCapSoftware:mainfrom
macakii327-prog:feat/deeplinks-raycast-extension

Conversation

@macakii327-prog
Copy link
Copy Markdown

@macakii327-prog macakii327-prog commented Mar 29, 2026

🎯 Bounty: #1540 — Deeplinks support + Raycast Extension

Deeplinks (Rust)

Extended DeepLinkAction enum with new actions:

  • PauseRecording — pause the current recording
  • ResumeRecording — resume a paused recording
  • TogglePauseRecording — toggle pause/resume
  • SwitchMicrophone { label } — switch to a different mic
  • SwitchCamera { camera } — switch to a different camera

All new actions delegate to existing functions (pause_recording, resume_recording, toggle_pause_recording, set_mic_input, set_camera_input).

Raycast Extension

Full Raycast extension at extensions/raycast/:

  • Start Recording — start screen recording via deeplink
  • Stop Recording — stop current recording
  • Toggle Pause — pause/resume recording
  • Open Cap — launch the app
  • Open Settings — open Cap settings
  • Recent Recordings — browse recordings with search

All commands use the cap-desktop://action?value=<json> deeplink scheme with correct serde serialization format.

Testing

  • Deeplink URL format matches existing TryFrom<&Url> parser
  • New Rust variants follow existing patterns (delegate to crate::recording::*)
  • Raycast commands are no-view for instant execution

Closes #1540

Greptile Summary

This PR extends Cap's deeplink system with five new Rust actions (PauseRecording, ResumeRecording, TogglePauseRecording, SwitchMicrophone, SwitchCamera) and ships a full Raycast extension that drives Cap via the cap-desktop:// deep link scheme. The Rust side is clean and follows established patterns well. The Raycast extension commands for start, stop, toggle-pause, open-cap, and open-settings are all correctly implemented. However, the Recent Recordings feature has two bugs that make it non-functional:

  • getRecordingsDir() returns $HOME/.cap/recordings, but Cap stores recordings under the Tauri app_data_dir() (~/Library/Application Support/<bundle-id>/recordings), so the directory is never found.
  • Recording paths are passed directly into file:// URLs without percent-encoding; paths containing spaces (e.g. Application Support) will produce malformed URLs that Cap's Rust URL parser will reject.

Confidence Score: 4/5

Safe to merge for the Rust deeplink additions and most Raycast commands; the Recent Recordings feature will be silently broken until the path and URL-encoding issues are fixed.

Two P1 issues affect the same feature (Recent Recordings): wrong recordings directory path and unencoded file:// URLs. All other commands work correctly. Rust changes are solid with no issues found.

extensions/raycast/src/utils.ts (getRecordingsDir path) and extensions/raycast/src/recent-recordings.tsx (file:// URL encoding and directory filtering)

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds PauseRecording, ResumeRecording, TogglePauseRecording, SwitchMicrophone, and SwitchCamera deeplink actions; each delegates cleanly to existing recording/input functions following established patterns.
extensions/raycast/src/utils.ts Core deeplink helper is correct; however getRecordingsDir() returns ~/.cap/recordings which does not match the actual Tauri app_data_dir path, breaking Recent Recordings entirely.
extensions/raycast/src/recent-recordings.tsx Reads wrong recordings directory and passes unencoded file paths as file:// URLs; both issues prevent recordings from being found or opened correctly.
extensions/raycast/src/start-recording.ts Sends correct serde-tagged struct deeplink to start recording; hardcodes 'Main Display' which may fail on systems with differently named displays, but is a reasonable default.
extensions/raycast/tsconfig.json Compiler options are appropriate, but $schema incorrectly points to the Raycast extension schema instead of the TypeScript config schema.

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant macOS
    participant Cap
    participant Recording

    User->>Raycast: Invoke command (e.g. Toggle Pause)
    Raycast->>Raycast: sendDeepLink("toggle_pause_recording")
    Raycast->>macOS: open cap-desktop://action?value=...
    macOS->>Cap: Deep link event
    Cap->>Cap: Parse URL into DeepLinkAction
    Cap->>Recording: toggle_pause_recording(app, state)
    Recording-->>Cap: Ok(())
    Raycast-->>User: showHUD("Toggled pause")
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 29-32

Comment:
**Incorrect recordings directory path**

`getRecordingsDir()` returns `$HOME/.cap/recordings`, but Cap (built with Tauri) stores recordings under the Tauri `app_data_dir()`, which on macOS resolves to `~/Library/Application Support/<bundle-identifier>/recordings` (e.g. `~/Library/Application Support/so.cap.desktop/recordings`). The hardcoded `~/.cap/recordings` path does not exist, so the **Recent Recordings** command will always return an empty list regardless of how many recordings exist.

The correct absolute path cannot be derived from the environment variable alone — it depends on the Tauri bundle identifier. One option is to expose the recordings directory via a dedicated deeplink/URL query so the extension can ask the app, or document the real path for users to override via a Raycast preference.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 59-61

Comment:
**Unencoded spaces in `file://` URL will break deep link handler**

`rec.path` is a raw filesystem path. On macOS the recordings directory lives under `~/Library/Application Support/…`, which contains a space. Passing `file:///Users/alice/Library/Application Support/…` as-is creates an invalid URL — the Rust `Url` parser will reject it (or produce unexpected results), so `OpenEditor` will never be triggered.

The path must be percent-encoded before constructing the URL:

```suggestion
                    await open(`file://${encodeURI(rec.path)}`);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29

Comment:
**`statSync` called twice per directory entry**

In the current implementation, every entry that passes the `isDirectory()` filter has `statSync` called again in the `map` step to retrieve `mtime`. This doubles the number of filesystem calls. A single `map`-then-filter pattern avoids the redundancy:

```suggestion
    return readdirSync(dir)
      .map((name) => {
        const fullPath = join(dir, name);
        const stat = statSync(fullPath);
        return { name, path: fullPath, stat };
      })
      .filter(({ stat }) => stat.isDirectory())
      .map(({ name, path, stat }) => ({ name, path, date: stat.mtime }))
      .sort((a, b) => b.date.getTime() - a.date.getTime())
      .slice(0, 50);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29

Comment:
**No filter for `.cap` directory extension**

`getRecordings()` lists all subdirectories in the recordings folder, including any non-recording artifacts. The Rust side treats only paths ending in `.cap` as valid recording projects (e.g. `"my-recording.cap"`). Adding a name filter keeps the list accurate:

```suggestion
    return readdirSync(dir)
      .filter((name) => {
        const fullPath = join(dir, name);
        return name.endsWith(".cap") && statSync(fullPath).isDirectory();
      })
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/raycast/tsconfig.json
Line: 2

Comment:
**Wrong `$schema` for `tsconfig.json`**

This file uses the Raycast _extension_ JSON schema (`https://www.raycast.com/schemas/extension.json`), which is intended for `package.json`. The TypeScript configuration schema should point to `"https://json.schemastore.org/tsconfig"` (or be omitted). Using the wrong schema means IDEs will validate this file against Raycast's extension manifest rules, not TypeScript's compiler options, and will flag valid options as errors.

```suggestion
  "$schema": "https://json.schemastore.org/tsconfig",
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: extend deeplinks + add Raycast ext..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Deeplinks (Rust):
- Add PauseRecording, ResumeRecording, TogglePauseRecording actions
- Add SwitchMicrophone and SwitchCamera actions
- All new actions delegate to existing recording/input functions

Raycast Extension:
- Start/Stop/Toggle Pause recording via deeplinks
- Open Cap app and settings
- Browse recent recordings with search
- All commands use cap-desktop:// URL scheme
- No-view commands for instant execution

Closes CapSoftware#1540
Comment on lines +29 to +32
export function getRecordingsDir(): string {
const home = process.env.HOME || "~";
return `${home}/.cap/recordings`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Incorrect recordings directory path

getRecordingsDir() returns $HOME/.cap/recordings, but Cap (built with Tauri) stores recordings under the Tauri app_data_dir(), which on macOS resolves to ~/Library/Application Support/<bundle-identifier>/recordings (e.g. ~/Library/Application Support/so.cap.desktop/recordings). The hardcoded ~/.cap/recordings path does not exist, so the Recent Recordings command will always return an empty list regardless of how many recordings exist.

The correct absolute path cannot be derived from the environment variable alone — it depends on the Tauri bundle identifier. One option is to expose the recordings directory via a dedicated deeplink/URL query so the extension can ask the app, or document the real path for users to override via a Raycast preference.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 29-32

Comment:
**Incorrect recordings directory path**

`getRecordingsDir()` returns `$HOME/.cap/recordings`, but Cap (built with Tauri) stores recordings under the Tauri `app_data_dir()`, which on macOS resolves to `~/Library/Application Support/<bundle-identifier>/recordings` (e.g. `~/Library/Application Support/so.cap.desktop/recordings`). The hardcoded `~/.cap/recordings` path does not exist, so the **Recent Recordings** command will always return an empty list regardless of how many recordings exist.

The correct absolute path cannot be derived from the environment variable alone — it depends on the Tauri bundle identifier. One option is to expose the recordings directory via a dedicated deeplink/URL query so the extension can ask the app, or document the real path for users to override via a Raycast preference.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +59 to +61
onAction={async () => {
await open(`file://${rec.path}`);
await showHUD("Opening in Cap...");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unencoded spaces in file:// URL will break deep link handler

rec.path is a raw filesystem path. On macOS the recordings directory lives under ~/Library/Application Support/…, which contains a space. Passing file:///Users/alice/Library/Application Support/… as-is creates an invalid URL — the Rust Url parser will reject it (or produce unexpected results), so OpenEditor will never be triggered.

The path must be percent-encoded before constructing the URL:

Suggested change
onAction={async () => {
await open(`file://${rec.path}`);
await showHUD("Opening in Cap...");
await open(`file://${encodeURI(rec.path)}`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 59-61

Comment:
**Unencoded spaces in `file://` URL will break deep link handler**

`rec.path` is a raw filesystem path. On macOS the recordings directory lives under `~/Library/Application Support/…`, which contains a space. Passing `file:///Users/alice/Library/Application Support/…` as-is creates an invalid URL — the Rust `Url` parser will reject it (or produce unexpected results), so `OpenEditor` will never be triggered.

The path must be percent-encoded before constructing the URL:

```suggestion
                    await open(`file://${encodeURI(rec.path)}`);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +18 to +29
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return statSync(fullPath).isDirectory();
})
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, date: stat.mtime };
})
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 statSync called twice per directory entry

In the current implementation, every entry that passes the isDirectory() filter has statSync called again in the map step to retrieve mtime. This doubles the number of filesystem calls. A single map-then-filter pattern avoids the redundancy:

Suggested change
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return statSync(fullPath).isDirectory();
})
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, date: stat.mtime };
})
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
return readdirSync(dir)
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, stat };
})
.filter(({ stat }) => stat.isDirectory())
.map(({ name, path, stat }) => ({ name, path, date: stat.mtime }))
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29

Comment:
**`statSync` called twice per directory entry**

In the current implementation, every entry that passes the `isDirectory()` filter has `statSync` called again in the `map` step to retrieve `mtime`. This doubles the number of filesystem calls. A single `map`-then-filter pattern avoids the redundancy:

```suggestion
    return readdirSync(dir)
      .map((name) => {
        const fullPath = join(dir, name);
        const stat = statSync(fullPath);
        return { name, path: fullPath, stat };
      })
      .filter(({ stat }) => stat.isDirectory())
      .map(({ name, path, stat }) => ({ name, path, date: stat.mtime }))
      .sort((a, b) => b.date.getTime() - a.date.getTime())
      .slice(0, 50);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +18 to +29
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return statSync(fullPath).isDirectory();
})
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, date: stat.mtime };
})
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No filter for .cap directory extension

getRecordings() lists all subdirectories in the recordings folder, including any non-recording artifacts. The Rust side treats only paths ending in .cap as valid recording projects (e.g. "my-recording.cap"). Adding a name filter keeps the list accurate:

Suggested change
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return statSync(fullPath).isDirectory();
})
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, date: stat.mtime };
})
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return name.endsWith(".cap") && statSync(fullPath).isDirectory();
})
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29

Comment:
**No filter for `.cap` directory extension**

`getRecordings()` lists all subdirectories in the recordings folder, including any non-recording artifacts. The Rust side treats only paths ending in `.cap` as valid recording projects (e.g. `"my-recording.cap"`). Adding a name filter keeps the list accurate:

```suggestion
    return readdirSync(dir)
      .filter((name) => {
        const fullPath = join(dir, name);
        return name.endsWith(".cap") && statSync(fullPath).isDirectory();
      })
```

How can I resolve this? If you propose a fix, please make it concise.

@@ -0,0 +1,16 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Wrong $schema for tsconfig.json

This file uses the Raycast extension JSON schema (https://www.raycast.com/schemas/extension.json), which is intended for package.json. The TypeScript configuration schema should point to "https://json.schemastore.org/tsconfig" (or be omitted). Using the wrong schema means IDEs will validate this file against Raycast's extension manifest rules, not TypeScript's compiler options, and will flag valid options as errors.

Suggested change
"$schema": "https://www.raycast.com/schemas/extension.json",
"$schema": "https://json.schemastore.org/tsconfig",
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/tsconfig.json
Line: 2

Comment:
**Wrong `$schema` for `tsconfig.json`**

This file uses the Raycast _extension_ JSON schema (`https://www.raycast.com/schemas/extension.json`), which is intended for `package.json`. The TypeScript configuration schema should point to `"https://json.schemastore.org/tsconfig"` (or be omitted). Using the wrong schema means IDEs will validate this file against Raycast's extension manifest rules, not TypeScript's compiler options, and will flag valid options as errors.

```suggestion
  "$schema": "https://json.schemastore.org/tsconfig",
```

How can I resolve this? If you propose a fix, please make it concise.

- Fix recordings directory path to use Tauri app data dir
  (~/Library/Application Support/so.cap.desktop/recordings)
- Percent-encode file:// URLs to handle spaces in paths
- Optimize statSync to single call per entry (map-then-filter)
- Filter recordings by .cap directory extension
- Fix tsconfig.json schema URL
@macakii327-prog
Copy link
Copy Markdown
Author

Hi! Just wanted to follow up — all the review feedback has been addressed in commit 46b82f2. The Vercel CI check seems to need authorization from the repo side. Would appreciate a review when you get a chance! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounty: Deeplinks support + Raycast Extension

1 participant